/**
 * BibSonomy-Rest-Client - The REST-client.
 *
 * Copyright (C) 2006 - 2016 Knowledge & Data Engineering Group,
 *                               University of Kassel, Germany
 *                               http://www.kde.cs.uni-kassel.de/
 *                           Data Mining and Information Retrieval Group,
 *                               University of Würzburg, Germany
 *                               http://www.is.informatik.uni-wuerzburg.de/en/dmir/
 *                           L3S Research Center,
 *                               Leibniz University Hannover, Germany
 *                               http://www.l3s.de/
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.bibsonomy.rest.client;

import static org.bibsonomy.util.ValidationUtils.present;

import java.net.URI;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bibsonomy.common.enums.ConceptUpdateOperation;
import org.bibsonomy.common.enums.Filter;
import org.bibsonomy.common.enums.GroupUpdateOperation;
import org.bibsonomy.common.enums.GroupingEntity;
import org.bibsonomy.common.enums.PostUpdateOperation;
import org.bibsonomy.common.enums.SearchType;
import org.bibsonomy.common.enums.TagRelation;
import org.bibsonomy.common.enums.TagSimilarity;
import org.bibsonomy.common.enums.UserRelation;
import org.bibsonomy.common.enums.UserUpdateOperation;
import org.bibsonomy.common.errors.ErrorMessage;
import org.bibsonomy.common.exceptions.DatabaseException;
import org.bibsonomy.model.Document;
import org.bibsonomy.model.Group;
import org.bibsonomy.model.GroupMembership;
import org.bibsonomy.model.Post;
import org.bibsonomy.model.Resource;
import org.bibsonomy.model.Tag;
import org.bibsonomy.model.User;
import org.bibsonomy.model.enums.GoldStandardRelation;
import org.bibsonomy.model.enums.Order;
import org.bibsonomy.model.logic.LogicInterface;
import org.bibsonomy.model.logic.util.AbstractLogicInterface;
import org.bibsonomy.model.sync.ConflictResolutionStrategy;
import org.bibsonomy.model.sync.SynchronizationData;
import org.bibsonomy.model.sync.SynchronizationDirection;
import org.bibsonomy.model.sync.SynchronizationPost;
import org.bibsonomy.model.sync.SynchronizationStatus;
import org.bibsonomy.model.util.PostUtils;
import org.bibsonomy.rest.RESTConfig;
import org.bibsonomy.rest.client.auth.AuthenticationAccessor;
import org.bibsonomy.rest.client.queries.delete.DeleteGroupQuery;
import org.bibsonomy.rest.client.queries.delete.DeletePostDocumentQuery;
import org.bibsonomy.rest.client.queries.delete.DeletePostQuery;
import org.bibsonomy.rest.client.queries.delete.DeleteSyncDataQuery;
import org.bibsonomy.rest.client.queries.delete.DeleteUserQuery;
import org.bibsonomy.rest.client.queries.delete.RemoveUserFromGroupQuery;
import org.bibsonomy.rest.client.queries.delete.UnpickClipboardQuery;
import org.bibsonomy.rest.client.queries.get.GetConceptDetailsQuery;
import org.bibsonomy.rest.client.queries.get.GetFriendsQuery;
import org.bibsonomy.rest.client.queries.get.GetGroupDetailsQuery;
import org.bibsonomy.rest.client.queries.get.GetGroupListQuery;
import org.bibsonomy.rest.client.queries.get.GetLastSyncDataQuery;
import org.bibsonomy.rest.client.queries.get.GetPostDetailsQuery;
import org.bibsonomy.rest.client.queries.get.GetPostDocumentQuery;
import org.bibsonomy.rest.client.queries.get.GetPostsQuery;
import org.bibsonomy.rest.client.queries.get.GetTagDetailsQuery;
import org.bibsonomy.rest.client.queries.get.GetTagRelationQuery;
import org.bibsonomy.rest.client.queries.get.GetTagsQuery;
import org.bibsonomy.rest.client.queries.get.GetUserDetailsQuery;
import org.bibsonomy.rest.client.queries.get.GetUserListOfGroupQuery;
import org.bibsonomy.rest.client.queries.get.GetUserListQuery;
import org.bibsonomy.rest.client.queries.post.AddUsersToGroupQuery;
import org.bibsonomy.rest.client.queries.post.CreateConceptQuery;
import org.bibsonomy.rest.client.queries.post.CreateGroupQuery;
import org.bibsonomy.rest.client.queries.post.CreatePostDocumentQuery;
import org.bibsonomy.rest.client.queries.post.CreatePostQuery;
import org.bibsonomy.rest.client.queries.post.CreateRelationQuery;
import org.bibsonomy.rest.client.queries.post.CreateSyncPlanQuery;
import org.bibsonomy.rest.client.queries.post.CreateUserQuery;
import org.bibsonomy.rest.client.queries.post.CreateUserRelationshipQuery;
import org.bibsonomy.rest.client.queries.post.PickPostQuery;
import org.bibsonomy.rest.client.queries.put.ChangeConceptQuery;
import org.bibsonomy.rest.client.queries.put.ChangeDocumentNameQuery;
import org.bibsonomy.rest.client.queries.put.ChangeGroupQuery;
import org.bibsonomy.rest.client.queries.put.ChangePostQuery;
import org.bibsonomy.rest.client.queries.put.ChangeSyncStatusQuery;
import org.bibsonomy.rest.client.queries.put.ChangeUserQuery;
import org.bibsonomy.rest.client.util.FileFactory;
import org.bibsonomy.rest.client.util.ProgressCallback;
import org.bibsonomy.rest.client.util.ProgressCallbackFactory;
import org.bibsonomy.rest.renderer.RendererFactory;
import org.bibsonomy.rest.renderer.RenderingFormat;
import org.bibsonomy.rest.renderer.UrlRenderer;
import org.bibsonomy.util.ExceptionUtils;

/**
 * {@link LogicInterface} for a remote BibSonomy/PUMA instance
 */
public class RestLogic extends AbstractLogicInterface {
	private static final Log log = LogFactory.getLog(RestLogic.class); // FIXME: who configs the logging?

	private static final User createUser(final String username, final String apiKey) {
		final User user = new User(username);
		user.setApiKey(apiKey);
		return user;
	}
	
	private final User authUser;
	private final AuthenticationAccessor accessor;

	private final String apiURL;
	private final RendererFactory rendererFactory;
	private final RenderingFormat renderingFormat;
	private final ProgressCallbackFactory progressCallbackFactory;
	
	private final FileFactory fileFactory;

	/**
	 * TODO: implement an {@link AuthenticationAccessor} for apikey access
	 * 
	 * @param username
	 *            the username
	 * @param apiKey
	 *            the API key
	 * @param apiURL
	 *            the API url
	 * @param renderingFormat
	 * @param progressCallbackFactory
	 */
	RestLogic(final String username, final String apiKey, final String apiURL, final RenderingFormat renderingFormat, final ProgressCallbackFactory progressCallbackFactory, final FileFactory fileFactory) {
		this(apiURL, renderingFormat, progressCallbackFactory, null, createUser(username, apiKey), fileFactory);
	}

	/**
	 * constructor using accessor instead of username and api key
	 * 
	 * @param accessor
	 * @param apiURL
	 * @param renderingFormat
	 * @param progressCallbackFactory
	 */
	RestLogic(final AuthenticationAccessor accessor, final String apiURL, final RenderingFormat renderingFormat, final ProgressCallbackFactory progressCallbackFactory, final FileFactory fileFactory) {
		this(apiURL, renderingFormat, progressCallbackFactory, accessor, new User(RESTConfig.USER_ME), fileFactory);
	}

	private RestLogic(final String apiURL, final RenderingFormat renderingFormat, final ProgressCallbackFactory progressCallbackFactory, final AuthenticationAccessor accessor, final User loggedinUser, final FileFactory fileFactory) {
		this.apiURL = apiURL;
		this.fileFactory = fileFactory;
		this.rendererFactory = new RendererFactory(new UrlRenderer(this.apiURL));
		this.renderingFormat = renderingFormat;
		this.progressCallbackFactory = progressCallbackFactory;

		this.authUser = loggedinUser;
		this.accessor = accessor;
	}

	private <T> T execute(final AbstractQuery<T> query) {
		try {
			query.setRenderingFormat(this.renderingFormat);
			query.setRendererFactory(this.rendererFactory);
			query.execute(this.authUser.getName(), this.authUser.getApiKey(), this.accessor);
		} catch (final Exception ex) {
			ExceptionUtils.logErrorAndThrowRuntimeException(log, ex, "unable to execute " + query.toString());
		}
		return query.getResult();
	}

	private <T> T executeWithCallback(final AbstractQuery<T> query, final ProgressCallback callback) {
		query.setProgressCallback(callback);
		return this.execute(query);
	}

	@Override
	public void deleteGroup(final String groupName, boolean pending, boolean quickDelete) {
		execute(new DeleteGroupQuery(groupName));
	}

	@Override
	public void deletePosts(final String userName, final List<String> resourceHashes) {
		/*
		 * FIXME: this iteration should be done on the server, i.e.,
		 * DeletePostQuery should support several posts ... although it's
		 * probably not so simple.
		 */
		for (final String resourceHash : resourceHashes) {
			execute(new DeletePostQuery(userName, resourceHash));
		}
	}

	@Override
	public void deleteUser(final String userName) {
		execute(new DeleteUserQuery(userName));
	}

	@Override
	public User getAuthenticatedUser() {
		return this.authUser;
	}

	@Override
	public Group getGroupDetails(final String groupName, final boolean pending) {
		return execute(new GetGroupDetailsQuery(groupName));
	}

	@Override
	public List<Group> getGroups(boolean pending, String userName, final int start, final int end) {
		if (pending) {
			throw new UnsupportedOperationException("quering for pending groups not supported");
		}
		return execute(new GetGroupListQuery(start, end));
	}

	@Override
	public Post<? extends Resource> getPostDetails(final String resourceHash, final String userName) {
		return execute(new GetPostDetailsQuery(userName, resourceHash));
	}
	
	@Override
	@SuppressWarnings("unchecked")
	public <T extends Resource> List<Post<T>> getPosts(final Class<T> resourceType, final GroupingEntity grouping, final String groupingName, final List<String> tags, final String hash, final String search, final SearchType searchType, final Set<Filter> filters, final Order order, final Date startDate, final Date endDate, final int start, final int end) {
		// TODO: properly implement searchtype in query and rest-server
		// TODO: clientside chain of responsibility
		final GetPostsQuery query = new GetPostsQuery(start, end);
		query.setGrouping(grouping, groupingName);
		query.setResourceHash(hash);
		query.setResourceType(resourceType);
		query.setTags(tags);
		query.setSearch(search);
		query.setOrder(order);
		query.setUserName(this.getAuthenticatedUser().getName());
		return (List) execute(query);
	}

	@Override
	public Tag getTagDetails(final String tagName) {
		return execute(new GetTagDetailsQuery(tagName));
	}
	
	@Override
	public List<Tag> getTagRelation(final int start, final int end, final TagRelation relation, final List<String> tagNames) {
		return execute(new GetTagRelationQuery(start, end, relation, tagNames));
	}

	@Override
	public List<Tag> getTags(final Class<? extends Resource> resourceType, final GroupingEntity grouping, final String groupingName, final List<String> tags, final String hash, final String search, final String regex, final TagSimilarity relation, final Order order, final Date startDate, final Date endDate, final int start, final int end) {
		return this.getTags(resourceType, grouping, groupingName, tags, hash, search, SearchType.LOCAL, regex, relation, order, startDate, endDate, start, end);
	}
	
	@Override
	public List<Tag> getTags(final Class<? extends Resource> resourceType, final GroupingEntity grouping, final String groupingName, final List<String> tags, final String hash, final String search, final SearchType searchType,final String regex, final TagSimilarity relation, final Order order, final Date startDate, final Date endDate, final int start, final int end) {
		final GetTagsQuery query = new GetTagsQuery(start, end);
		query.setResourceType(resourceType);
		query.setGrouping(grouping, groupingName);
		query.setFilter(regex);
		query.setOrder(order);
		return execute(query);
	}

	@Override
	public User getUserDetails(final String userName) {
		return execute(new GetUserDetailsQuery(userName));
	}

	@Override
	public String createGroup(final Group group) {
		return execute(new CreateGroupQuery(group));
	}

	@Override
	public List<String> createPosts(final List<Post<?>> posts) {
		/*
		 * FIXME: this iteration should be done on the server, i.e.,
		 * CreatePostQuery should support several posts ... although it's
		 * probably not so simple.
		 */
		final List<String> resourceHashes = new LinkedList<String>();
		for (final Post<?> post : posts) {
			final String hash = execute(new CreatePostQuery(this.authUser.getName(), post));
			if (present(hash)) {
				resourceHashes.add(hash);
			}
		}
		return resourceHashes;
	}

	@Override
	public String createUser(final User user) {
		return execute(new CreateUserQuery(user));
	}
	
	// TODO: Establish new group concept in here.
	@Override
	public String updateGroup(final Group group, final GroupUpdateOperation operation, GroupMembership ms) {
		final String groupName = group.getName();
		switch (operation) {
			case ADD_MEMBER:
				return execute(new AddUsersToGroupQuery(groupName, Collections.singletonList(ms)));
			case REMOVE_MEMBER:
				return execute(new RemoveUserFromGroupQuery(ms.getUser().getName(), groupName));
			default:
				return execute(new ChangeGroupQuery(groupName, group));
		}
	}

	@Override
	public List<String> updatePosts(final List<Post<?>> posts, final PostUpdateOperation operation) {
		/*
		 * FIXME: this iteration should be done on the server, i.e.,
		 * CreatePostQuery should support several posts ... although it's
		 * probably not so simple.
		 */
		final List<String> resourceHashes = new LinkedList<String>();
		final DatabaseException collectedException = new DatabaseException();
		for (final Post<?> post : posts) {
			final ChangePostQuery query = new ChangePostQuery(this.authUser.getName(), post.getResource().getIntraHash(), post);
			final String hash = execute(query);
			if (!query.isSuccess()) {
				collectedException.addToErrorMessages(PostUtils.getKeyForPost(post), new ErrorMessage(hash, hash));
			}
			// hashes are recalculated by the server
			resourceHashes.add(hash);
		}
		if (collectedException.hasErrorMessages()) {
			throw collectedException;
		}
		return resourceHashes;
	}

	@Override
	public String updateUser(final User user, final UserUpdateOperation operation) {
		// accounts cannot be renamed
		return execute(new ChangeUserQuery(user.getName(), user));
	}

	@Override
	public String createDocument(final Document doc, final String resourceHash) {
		if (!present(doc.getUserName())) {
			doc.setUserName(this.authUser.getName());
		}
		
		if (!present(doc.getFileName())) {
			doc.setFileName(doc.getFile().getName());
		}
		final CreatePostDocumentQuery createPostDocumentQuery = new CreatePostDocumentQuery(doc, resourceHash);
		return execute(createPostDocumentQuery);
	}
	
	@Override
	public Document getDocument(final String userName, final String resourceHash, final String fileName) {
		return executeWithCallback(new GetPostDocumentQuery(userName, resourceHash, fileName, fileFactory), this.progressCallbackFactory.createDocumentDownloadProgressCallback());
	}

	@Override
	public void deleteDocument(final Document document, final String resourceHash) {
		final DeletePostDocumentQuery deletePostDocumentQuery = new DeletePostDocumentQuery(document.getUserName(), resourceHash, document.getFileName());
		execute(deletePostDocumentQuery);
	}

	@Override
	public String createConcept(final Tag concept, final GroupingEntity grouping, final String groupingName) {
		return execute(new CreateConceptQuery(concept, concept.getName(), grouping, groupingName));
	}

	@Override
	public String updateConcept(final Tag concept, final GroupingEntity grouping, final String groupingName, final ConceptUpdateOperation operation) {
		switch(operation) {
		case PICK:
			throw new UnsupportedOperationException();
		case PICK_ALL:
			throw new UnsupportedOperationException();
		case UNPICK:
			throw new UnsupportedOperationException();
		case UNPICK_ALL:
			throw new UnsupportedOperationException();
		case UPDATE:
			return execute(new ChangeConceptQuery(concept, concept.getName(), grouping, groupingName));
		default:
			throw new UnsupportedOperationException();
		}
	}

	@Override
	public Tag getConceptDetails(final String conceptName, final GroupingEntity grouping, final String groupingName) {
		final GetConceptDetailsQuery query = new GetConceptDetailsQuery(conceptName);
		
		if ((grouping == null) || (GroupingEntity.ALL.equals(grouping))	|| (present(groupingName)
				&& (GroupingEntity.GROUP.equals(grouping) || GroupingEntity.USER.equals(grouping)))) {
			query.setGrouping(grouping, groupingName);
			return execute(query);
		}

		log.error("grouping entity " + grouping.name() + " not yet supported in RestLogic implementation.");
		return null;
	}

	@Override
	public List<User> getUsers(final Class<? extends Resource> resourceType, final GroupingEntity grouping, final String groupingName, final List<String> tags, final String hash, final Order order, final UserRelation relation, final String search, final int start, final int end) {
		// here we just simulate two possible answers of the user chain
		if (GroupingEntity.ALL.equals(grouping)) {
			return execute(new GetUserListQuery(start, end));
		}
		if (GroupingEntity.GROUP.equals(grouping)) {
			return execute(new GetUserListOfGroupQuery(groupingName, start, end));
		}
		log.error("grouping entity " + grouping.name() + " not yet supported in RestLogic implementation.");
		return null;
	}

	@Override
	public void createUserRelationship(final String sourceUser, final String targetUser, final UserRelation relation, final String tag) {
		/*
		 * Transform UserRelation into String. FIXME: shouldn't we do this in a
		 * nicer way?
		 */
		final String relationType;
		switch (relation) {
		case OF_FRIEND:
			relationType = CreateUserRelationshipQuery.FRIEND_RELATIONSHIP;
			break;
		case FOLLOWER_OF:
			relationType = CreateUserRelationshipQuery.FOLLOWER_RELATIONSHIP;
			break;
		default:
			throw new IllegalArgumentException("Only OF_FRIEND (for friend relations) and FOLLOWER_OF (for followers) are allowed values for the relation param.");
		}
		execute(new CreateUserRelationshipQuery(sourceUser, targetUser, relationType, tag));
	}

	@Override
	public List<User> getUserRelationship(final String sourceUser, final UserRelation relation, final String tag) {
		switch (relation) {
		case FRIEND_OF:
			return execute(new GetFriendsQuery(sourceUser, RESTConfig.OUTGOING_ATTRIBUTE_VALUE_RELATION, 0, 100));
		case OF_FRIEND:
			return execute(new GetFriendsQuery(sourceUser, RESTConfig.INCOMING_ATTRIBUTE_VALUE_RELATION, 0, 100));
		default:
			throw new UnsupportedOperationException("The user relation " + relation + " is currently not supported.");
		}
	}

	@Override
	public int createClipboardItems(final List<Post<? extends Resource>> posts) {
		final PickPostQuery query = new PickPostQuery();
		query.setUserName(posts.get(0).getUser().getName());
		query.setResourceHash(posts.get(0).getResource().getIntraHash());
		return execute(query).intValue();
	}

	@Override
	public int deleteClipboardItems(final List<Post<? extends Resource>> posts, final boolean clearAll) {
		final UnpickClipboardQuery query = new UnpickClipboardQuery();
		query.setClearAll(clearAll);

		if (present(posts)) {
			query.setUserName(posts.get(0).getUser().getName());
			query.setResourceHash(posts.get(0).getResource().getIntraHash());
		}

		return execute(query).intValue();
	}
	
	@Override
	public void createRelations(final String postHash, final Set<String> references, final GoldStandardRelation relation) {
		if (!present(postHash) || !present(references) || !present(relation)) {
			// FIXME: who needs/reads this warning? 
			log.warn("can't create references because no post hash/ no references/ no relation given");
			return;
		}
		for (final String referenceHash : references) {
			final CreateRelationQuery query = new CreateRelationQuery(postHash, referenceHash, relation);
			execute(query);
		}
	}
	
	@Override
	public void updateSyncData(final String userName, final URI service, final Class<? extends Resource> resourceType, final Date syncDate, final SynchronizationStatus status, final String info, Date newSyncDate) {
		this.execute(new ChangeSyncStatusQuery(service.toString(), resourceType, null, null, status, info, newSyncDate));
	}

	@Override
	public void deleteSyncData(final String userName, final URI service, final Class<? extends Resource> resourceType, final Date syncDate) {
		this.execute(new DeleteSyncDataQuery(service.toString(), resourceType, syncDate, null, null));
	}

	@Override
	public SynchronizationData getLastSyncData(final String userName, final URI service, final Class<? extends Resource> resourceType) {
		return this.execute(new GetLastSyncDataQuery(service.toString(), resourceType, null, null));
	}

	@Override
	public List<SynchronizationPost> getSyncPlan(final String userName, final URI service, final Class<? extends Resource> resourceType, final List<SynchronizationPost> clientPosts, final ConflictResolutionStrategy strategy, final SynchronizationDirection direction) {
		return this.execute(new CreateSyncPlanQuery(service.toString(), clientPosts, resourceType, strategy, direction));
	}
	
	@Override
	public void updateDocument(String userName, final String resourceHash, String documentName, final Document document) {
		if (!present(document.getUserName())) {
			document.setUserName(this.authUser.getName());
		}
		
		this.execute(new ChangeDocumentNameQuery(userName, resourceHash, documentName, document));
	}
	
	/* (non-Javadoc)
	 * @see org.bibsonomy.model.logic.util.AbstractLogicInterface#doDefaultAction()
	 */
	@Override
	protected void doDefaultAction() {
		throw new UnsupportedOperationException();
	}
}